/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 2007-2009  Intel Corporation. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <errno.h>
#include <glib.h>
#include <string.h>
#include <unistd.h>
#include <net/if_arp.h>
#include <sys/socket.h>
#define CONNMAN_API_SUBJECT_TO_CHANGE
#include <connman/dbus.h>
#include <connman/device.h>
#include <connman/inet.h>
#include <connman/ipconfig.h>
#include <connman/log.h>
#include <connman/plugin.h>
#include <connman/task.h>

#define	_DBG_PPPD(fmt, arg...)	DBG(DBG_PPPD, fmt, ## arg)

#define PPPD_INTF "org.samba.pppd"
#define PPPD_PATH "/org/samba/pppd"

struct pppd_priv {
	DBusConnection *connection;
	struct connman_task *task;
	struct connman_ipconfig *ipconfig;
};

static const char *find_pppd(void)
{
	static const char *pppd_locations[] = {
		"/usr/sbin/pppd",
		"/sbin/pppd"
	};
	int i;

	for (i = 0; i < sizeof(pppd_locations)/sizeof(const char *); i++) {
		if (g_file_test(pppd_locations[i], G_FILE_TEST_IS_EXECUTABLE))
			return pppd_locations[i];
	}
	return NULL;
}

static const char *pppdargs[] = {
	"921600",	/* TODO(ers): find the baud rate */
	"nodetach",
	"crtscts",	/* TODO(ers): device dependent? */
	"lock",
	"user", "user",	/* TODO(ers) find user name */
	"password", "user",	/* TODO(ers) find password */
	"fetchpeerdns",
	"noauth",
	"debug"		/* TODO(ers) temporary */
};

static DBusHandlerResult pppd_filter(DBusConnection *conn,
    DBusMessage *msg, void *user_data);
static void pppd_died(struct connman_task *task, void *user_data);

static const char *pppd_rule = "path=" PPPD_PATH ",interface=" PPPD_INTF;

static int pppd_start(struct connman_ipconfig *ipconfig)
{
	struct connman_device *device;
	const char *tty;
        int i;
	struct connman_task *task;
	struct pppd_priv *pppd_data;
	const char *busname;
	const char *pppd_binary;

	_DBG_PPPD("");
	pppd_binary = find_pppd();
	if (pppd_binary == NULL)
		return -ENOENT;

	task = connman_task_create(pppd_binary);
	if (task == NULL)
		return -ENOMEM;

	device = connman_ipconfig_get_device(ipconfig);
	if (device == NULL) {
		connman_task_destroy(task);
		_DBG_PPPD("Cannot get device");
		return -ENODEV;
	}

	tty = connman_device_get_string(device, "Tty");
	if (tty == NULL) {
		connman_task_destroy(task);
		_DBG_PPPD("Device missing Tty Property");
		return -ENODEV;
	}

	pppd_data = g_try_new0(struct pppd_priv, 1);
	if (pppd_data == NULL) {
		connman_task_destroy(task);
		return -ENOMEM;
	}

	pppd_data->connection = connman_dbus_get_connection();
	if (pppd_data->connection == NULL) {
		connman_task_destroy(task);
		g_free(pppd_data);
		return -EIO;
	}
	dbus_connection_add_filter(pppd_data->connection, pppd_filter, pppd_data, NULL);
	dbus_bus_add_match(pppd_data->connection, pppd_rule, NULL);

	busname = dbus_bus_get_unique_name(pppd_data->connection);

	connman_task_add_argument(task, tty, NULL);
	for (i = 0; i < sizeof(pppdargs)/sizeof(const char *); i++)
		connman_task_add_argument(task, pppdargs[i], NULL);
	connman_task_add_argument(task, "ipparam", "%s", busname);

	connman_task_run(task, pppd_died, pppd_data, NULL, NULL, NULL);
	pppd_data->task = task;
	pppd_data->ipconfig = ipconfig;
	connman_ipconfig_set_data(ipconfig, pppd_data);
	return 0;
}

static int pppd_stop(struct connman_ipconfig *ipconfig)
{
	struct pppd_priv *pppd_data = connman_ipconfig_get_data(ipconfig);

	_DBG_PPPD("");
	/*
	 * If pppd is not running, the following is a no-op.
	 * If it is running, then the rest of the cleanup steps
	 * will be handled in pppd_died.
	 */
	if (pppd_data != NULL)
		connman_task_stop(pppd_data->task);
	return 0;
}

static void pppd_died(struct connman_task *task, void *user_data)
{
	struct pppd_priv *pppd_data = user_data;

	_DBG_PPPD("");
	dbus_bus_remove_match(pppd_data->connection, pppd_rule, NULL);
	dbus_connection_remove_filter(pppd_data->connection, pppd_filter, pppd_data);
	dbus_connection_unref(pppd_data->connection);
	connman_ipconfig_set_data(pppd_data->ipconfig, NULL);
	g_free(pppd_data);
	connman_task_destroy(task);
}

static struct connman_ipconfig_driver ppp_driver = {
	.name		= "ppp",
	.type		= CONNMAN_IPCONFIG_TYPE_PPP,
	.priority	= CONNMAN_IPCONFIG_PRIORITY_DEFAULT,
	.request	= pppd_start,
	.release        = pppd_stop
	/* NB: nothing to do for renew */
};

static DBusHandlerResult pppd_filter(DBusConnection *conn,
    DBusMessage *msg, void *user_data)
{
	DBusMessageIter iter, dict;
	dbus_uint32_t pid;
	const char *key;
	char *dns1 = NULL;
	char *dns2 = NULL;
	const char *interface = NULL;
	char *value;
	struct connman_ipaddress ipaddr;
	struct connman_device *device;
	struct pppd_priv *pppd_data = user_data;
	int index;

	if (dbus_message_is_method_call(msg, PPPD_INTF, "Notify") == FALSE)
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;

	dbus_message_iter_init(msg, &iter);

	dbus_message_iter_get_basic(&iter, &pid);
	dbus_message_iter_next(&iter);

	memset(&ipaddr, 0, sizeof(ipaddr));
	ipaddr.af = AF_INET;
	ipaddr.mask |= CONNMAN_IPCONFIG_AF;

	dbus_message_iter_recurse(&iter, &dict);
	while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry;

		dbus_message_iter_recurse(&dict, &entry);
		dbus_message_iter_get_basic(&entry, &key);
		dbus_message_iter_next(&entry);
		dbus_message_iter_get_basic(&entry, &value);

		_DBG_PPPD("%s = %s", key, value);


		if (g_ascii_strcasecmp(key, "iplocal") == 0) {
			ipaddr.local = g_strdup(value);
			ipaddr.mask |= CONNMAN_IPCONFIG_LOCAL;
		} else if (g_ascii_strcasecmp(key, "ipremote") == 0) {
			ipaddr.peer = g_strdup(value);
			ipaddr.mask |= CONNMAN_IPCONFIG_PEER;
		} else if (g_ascii_strcasecmp(key, "dns1") == 0)
			dns1 = g_strdup(value);
		else if (g_ascii_strcasecmp(key, "dns2") == 0)
			dns2 = g_strdup(value);
		else if (g_ascii_strcasecmp(key, "interface") == 0)
			interface = g_strdup(value);
		else if (g_ascii_strcasecmp(key, "device") == 0)
			;
		else if (g_ascii_strcasecmp(key, "tty") == 0)
			;
		else if (g_ascii_strcasecmp(key, "speed") == 0)
			;

		dbus_message_iter_next(&dict);
	}
	if (interface == NULL) {
		connman_error("pppd did not send interface name");
		g_free(ipaddr.local);
		g_free(ipaddr.peer);
		g_free(dns1);
		g_free(dns2);
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
	}
	index = connman_inet_ifindex(interface);
	connman_ipconfig_set_index(pppd_data->ipconfig, index);
	device = connman_ipconfig_get_device(pppd_data->ipconfig);
	connman_device_set_index(device, index);
	connman_device_set_interface(device, interface, NULL);
	ipaddr.gateway = g_strdup(ipaddr.peer);
	ipaddr.mask |= CONNMAN_IPCONFIG_GW;
	ipaddr.broadcast = g_strdup("0.0.0.0");
	ipaddr.mask |= CONNMAN_IPCONFIG_BCAST;
	ipaddr.domain_name = g_strdup("");	/* TODO(ers) fixme */
	ipaddr.mask |= CONNMAN_IPCONFIG_DOMAIN;
	ipaddr.prefixlen = 32;
	ipaddr.mask |= CONNMAN_IPCONFIG_PREFIX;
	ipaddr.dns_servers = g_try_new0(gchar *, 3);
	if (dns1 != NULL) {
		ipaddr.dns_servers[0] = dns1;
		if (dns2 != NULL)
			ipaddr.dns_servers[1] = dns2;
		ipaddr.mask |= CONNMAN_IPCONFIG_DNS;
	}

	connman_ipconfig_bind(pppd_data->ipconfig, &ipaddr);
	g_strfreev(ipaddr.dns_servers);
	return DBUS_HANDLER_RESULT_HANDLED;
}

static int pppd_init(void)
{
	_DBG_PPPD("");
	return connman_ipconfig_driver_register(&ppp_driver);
}

static void pppd_exit(void)
{
	_DBG_PPPD("");
	connman_ipconfig_driver_unregister(&ppp_driver);
}

CONNMAN_PLUGIN_DEFINE(pppd, "Point-to-point protocol plugin", VERSION,
		CONNMAN_PLUGIN_PRIORITY_DEFAULT, pppd_init, pppd_exit)
